[Day19]「吃下惡魔果實,什麼 Block 都能變成物件!」
不得不說尾田老師真的是常常腦洞大開,居然還有吃了狗狗果實的槍還有吃了象象果實的劍!
前幾天提到了 Block 不能在程式裡單獨存在,不過,今天要介紹的 Proc
和 Lambda
,在某些地方又被稱為「匿名函數」,它們可以幫忙把 Block 拿出來變成物件使用。
簡單來說,Proc
是一種「物件化」的 Block,如果稍微忘記物件的朋友可以先坐時光機回到 第 10 天 再回來繼續看喔!
物件化?具現化?
從物件導向的角度切入,當我們需要 Block 可以被傳遞,並作為一個 receiver
來接受訊息&使用方法,這時候就會用 Proc
對 Block 進行物件化,我們直接看程式碼吧!
一般的方法只能被呼叫:
def add_two()
2 + n
end
add_two(3)
但如果透過 Proc.new
產生一個 Proc
物件,就可以呼叫其他方法了:
add_two = Proc.new { |x| x * 2 }
add_two.call(3)
除了呼叫方法之外,也能在程式裡被傳來傳去了!變成了 Proc
以後真是好處多多啊!
list = [1, 2, 3, 4, 5]
double = Proc.new { |x| x * 2 }
p list.map { |element| double.call(element) }
# 印出
[2, 4, 6, 8, 10]
Proc
不小心忘了寫 new
好!現在我們已經知道要用 Proc.new
把一個 Block 物件化:
double = Proc.new { x * 2 }
# 印出
#<Proc:0x00007fa4592659a8>
有時候可能會忘了 new
,不小心寫成 proc
:
double = proc { x * 2 }
# 印出
#<Proc:0x00007fa459284e48>
別擔心這樣也可以!
接著要介紹一個和 Proc
很像的東西:Lambda
,它的寫法是這樣:
double = -> { x * 2 }
#<Proc:0x00007fa4598f5de0 (lambda)>
如果 Javascript 寫習慣了想用 ->
也行:
double = -> { x * 2 }
# 印出
#<Proc:0x00007fa45a0acac0 (lambda)>
Ruby 還會貼心提醒你這是個 lambda
:)
Lambda
和 Proc
有什麼不同?兩個真的很像吧?Lambda
的效果就和 Proc
一樣,都能把 Block 變成一個獨立的物件,但兩者還是有差異的,否則幹嘛要設計兩個名詞自找麻煩呢?
Lambda
和 Proc
的差異主要有二:
Lambda
和 Proc
回傳值的方式不同這邊借五倍的範例來做解說:
def double(callable_object)
callable_object.call * 2
end
la = lambda { return 10 }
pr = proc { return 10 }
double(la)
=> 20
puts double(pr)
=> LocalJumpError (unexpected return)
從以上結果發現 Lambda
可以被當作帶入方法的參數,而 Proc
就無法辦到,但這並不是 Proc
不能執行或沒有回傳值,而是因為 Proc
在回傳值時,是從定義 Proc
時的 scope 回傳。
嗯...什麼意思?
再繼續看這個:
def lambda_double
la = lambda { return 10 }
result = la.call
return result * 2
end
def proc_double
pr = Proc.new { return 10 }
result = pr.call
return result * 2 # unreachable code!
end
現在把 Lambda
和 Proc
放在方法裡,我預期 lambda_double
和 proc_double
應該都會得到 20 這個結果,但事實上:
lambda_double
=> 20
puts proc_double
=> 10
我們從這個例子可以更明顯地看到 Lambda
和 Proc
的不同,Lambda
的回傳值可以離開 Lambda
本身給其他程式碼使用,但 Proc
就只能在一開始定義這個 Proc
的那行回傳。
Lambda
和 Proc
參數判斷的方式不同如果我們給 Proc
的參數數量不對:
pr = proc {|a,b| [a,b]}
pr.call('a', 'b')
=> ["a", "b"]
pr.call('a')
=> ["a", nil]
pr.call('a', 'b', 'c')
=> ["a", "b"]
可以發現,如果參數給的比預期多 Proc
會自動忽略;如果比預期少的話,則會補一個 nil
,基本上都還是可以動,這點和 JavaScript 蠻像的!
相對來說,Lambda
在參數數量這點上就比較嚴格,數量必須要給正確才行:
la = lambda {|a,b| [a,b]}
p la.call('a', 'b')
=> ["a", "b"]
p la.call('a')
=> ArgumentError (wrong number of arguments (given 1, expected 2))
p la.call('a', 'b', 'c')
=> ArgumentError (wrong number of arguments (given 3, expected 2))
&
?這是什麼?最後來看一個比較特殊的情形,先直接看 code:
list1 = [1, 2, 3, 4, 5]
list2 = ["a", "b", "c"]
double = -> (x) { x * 2 }
p list1.map(&double)
# 印出
[2, 4, 6, 8, 10]
咦咦咦!這什麼?
第一次看到時,差點以為要被毀三觀,更冒出一堆黑人問號:
map
後面不是要加 Block 嗎?double
在這裡不是一個 Lambda
物件嗎?&
又是什麼?在經過一番消化理解後,才發現原來在這裡是把 double
這個Lambda
物件前面如果加上了 &
,就能又轉成了 Block 接在方法後面使用(寫法卻已經大大不同了!)真的是翻了很多文章才看出一點端倪(盯)
今天翻閱參考文章時,還有看到一個很特別的用 &
+ symbol 的寫法!我花點時間弄清楚後會再貼上來分享給大家!
在 Javascript 的世界裡,function
具有 higher order 的特性,因此可以被當作參數丟來丟去,不過在 Ruby 的世界裡就沒有此特性,必須先寫成 Proc
或 Lambda
才能達成,不過其實還好,因為 Ruby 方法已經很好用了,每個語言本就有各自的強項和弱項,這和程式語言的設計哲學有很大的關聯。
呼~今天這篇真的是花很多時間在寫,都沒時間找梗圖了!希望自己對 Proc
還有 Lambda
的理解還算正確,如有說明不清楚的地方,歡迎大家留言在下方或給我一些寫作上的建議,感謝!